package ar.uba.fi.tonyvaliente.signature;

import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Properties;
import java.util.Set;

/**
 * Clase que representa la configuracion de signature files
 * a ser usada por la aplicacion
 * @author dmorello
 */
public class SignatureConfiguration {
	
	private int signatureLength;
	private Set<HashFunction> hashFunctions;

	private static final String SIGNATURE_PROPERTY_ROOT = "signature.";
	private static final String FUNCTION_PROPERTY_ROOT = "function.";
	
	private static final String SIGNATURE_LENGTH_KEY = SIGNATURE_PROPERTY_ROOT + "length";
	private static final String FUNCTION_NUMBER_KEY = SIGNATURE_PROPERTY_ROOT + "functionnumber";
	private static final String FUNCTION_NAME_KEY_ROOT = FUNCTION_PROPERTY_ROOT + "name";
	private static final String FUNCTION_SEED_KEY_ROOT = FUNCTION_PROPERTY_ROOT + "seed";
	
	/**
	 * Crea una instancia a partir de la configuracion leida de un
	 * archivo properties cuyo nombre recibe por parametro.
	 *  
	 * @param path Ruta del archivo del cual cargar la configuracion
	 * @return instancia con la configuracion leida
	 * @throws SignatureConfigurationException si ocurre algun error
	 */
	public static SignatureConfiguration loadFromFile(String path)
			throws SignatureConfigurationException {
		
		// Leo las propiedades del archivo "path"
		Properties signatureProps = new Properties();
		Properties functionProps = new Properties();
		try {
			FileInputStream in = new FileInputStream(path);
			Properties props = new Properties();
			props.load(in);
			
			String key, value;
			Enumeration<?> keys = props.propertyNames();
			while (keys.hasMoreElements()) {
				key = (String)keys.nextElement();
				value = props.getProperty(key);
				if (key.startsWith(SIGNATURE_PROPERTY_ROOT)) {
					signatureProps.setProperty(key, value);
				} else if (key.startsWith(FUNCTION_PROPERTY_ROOT)) {
					functionProps.setProperty(key, value);
				} else {
					throw new SignatureConfigurationException("Configuracion no valida - "
							+ "Propiedad no reconocida: " + key);
				}
			}
		} catch (IOException e) {
			throw new SignatureConfigurationException("Error de lectura de archivo", e);
		}
		
		// Obtengo las propiedades de la firma
		int signatureLength;
		int functionNumber;
		if (!signatureProps.containsKey(SIGNATURE_LENGTH_KEY)) {
			throw new SignatureConfigurationException("Configuracion no valida - "
					+ "No se especifica longitud de la firma");
		}
		if (!signatureProps.containsKey(FUNCTION_NUMBER_KEY)) {
			throw new SignatureConfigurationException("Configuracion no valida - "
					+ "No se especifica cantidad de funciones de hash");
		}
		try {
			signatureLength = Integer.parseInt(signatureProps.getProperty(SIGNATURE_LENGTH_KEY));
			functionNumber = Integer.parseInt(signatureProps.getProperty(FUNCTION_NUMBER_KEY));
		} catch (NumberFormatException e) {
			throw new SignatureConfigurationException("Configuracion no valida", e);
		}
		
		// Obtengo las propiedades de las funciones
		Set<HashFunction> hashFunctions = null;
		HashFunction[] functionArray = new HashFunction[functionNumber];
		Integer[] seedArray = new Integer[functionNumber];
		try {
			// Recorro las propiedades y valido que sean correctas
			String key, value;
			Enumeration<?> keys = functionProps.propertyNames();
			while (keys.hasMoreElements()) {
				key = (String)keys.nextElement();
				if (key.startsWith(FUNCTION_NAME_KEY_ROOT)) {
					// Nombre de la clase hija de HashFunction
					String strNumber = key.substring(FUNCTION_NAME_KEY_ROOT.length());
					int number = Integer.parseInt(strNumber);
					if (number < functionArray.length) {
						if (functionArray[number] == null) {
							value = functionProps.get(key).toString();
							functionArray[number] = createInstance(value, signatureLength);
						} else {
							throw new SignatureConfigurationException("Configuracion no valida - "
									+ "Numero de funcion de hash repetido: " + number);
						}
					} else {
						throw new SignatureConfigurationException("Configuracion no valida - "
								+ "Numero de funcion de hash incorrecto: " + number);
					}
				} else if (key.startsWith(FUNCTION_SEED_KEY_ROOT)) {
					String strNumber = key.substring(FUNCTION_SEED_KEY_ROOT.length());
					int number = Integer.parseInt(strNumber);
					if (number < functionArray.length) {
						if (seedArray[number] == null) {
							value = functionProps.get(key).toString();
							seedArray[number] = Integer.valueOf(value);
						} else {
							throw new SignatureConfigurationException("Configuracion no valida - "
									+ "Numero de semilla repetido: " + number);
						}
					} else {
						throw new SignatureConfigurationException("Configuracion no valida - "
								+ "Numero de funcion de hash incorrecto: " + number);
					}
				} else {
					// Otra clave desconocida -> Error
					throw new SignatureConfigurationException("Configuracion no valida - "
							+ "clave: " + key);
				}
			}
			
			// Armo la coleccion de funciones de hash con semilla
			// Para ello, el elemento i de functionArray y seedArray debe ser no nulo
			hashFunctions = new HashSet<HashFunction>();
			for (int i = 0; i < functionArray.length; i++) {
				if (functionArray[i] != null && seedArray[i] != null) {
					functionArray[i].setSeed(seedArray[i]);
					hashFunctions.add(functionArray[i]);
				}
			}
		} catch (NumberFormatException e) {
			throw new SignatureConfigurationException("Valor numerico incorrecto - ", e);
		}
		
		return new SignatureConfiguration(signatureLength, hashFunctions);
	}
	
	/**
	 * Escribe la configuracion en un archivo properties cuyo nombre recibe
	 * por parametro.
	 * @param path Nombre del archivo donde escribir la configuracion
	 * @throws SignatureConfigurationException si ocurre un error
	 */
	public void writeToFile(String path) throws SignatureConfigurationException {
		
		// Creo objeto Properties con la configuracion
		Properties props = new Properties();
		props.put(SIGNATURE_LENGTH_KEY, Integer.toString(this.signatureLength));
		props.put(FUNCTION_NUMBER_KEY, Integer.toString(this.hashFunctions.size()));
		
		int i = 0;
		String key, value;
		
		for (HashFunction f: hashFunctions) {
			// Escribo nombre de la funcion 
			key = FUNCTION_NAME_KEY_ROOT + Integer.toString(i);
			value = f.getClass().getName();
			props.put(key, value);
			
			// Escribo valor de la semilla
			key = FUNCTION_SEED_KEY_ROOT + Integer.toString(i);
			value = Integer.toString(f.getSeed());
			props.put(key, value);
			
			i++;
		}
		
		// Escribo en archivo
		try {
			FileWriter writer = new FileWriter(path);
			props.store(writer,
					"Ultra Mini Basic Text Search Engine - "
					+ "Configuracion de signature files");
			writer.close();
		} catch (IOException e) {
			throw new SignatureConfigurationException("Error al escribir en archivo", e);
		}
		
	}
	
	/**
	 * Crea una instancia de la clase con los valores recibidos por parametro
	 * @param signatureLength Longitud de la firma, debe ser positiva
	 * @param hashFunctions Conjunto de funciones de hash a utilizar,
	 * no debe ser vacio.
	 * @throws SignatureConfigurationException Si alguno de los parametros
	 * no es valido.
	 */
	public SignatureConfiguration(int signatureLength, Set<HashFunction> hashFunctions)
			throws SignatureConfigurationException {
		
		// Valido que la longitud de la firma sea positiva
		if (signatureLength <= 0) {
			throw new SignatureConfigurationException("Tamaño de signature no valido: "
					+ signatureLength);
		}
		
		// Valido que haya funciones de hash en el set
		if (hashFunctions.isEmpty()) {
			throw new SignatureConfigurationException("No se especificaron funciones de hash");
		}
		
		// Asigno los atributos de la instancia
		this.signatureLength = signatureLength;
		this.hashFunctions = hashFunctions;
	}
	
	/**
	 * Devuelve la longitud de la firma
	 * @return longitud de la firma
	 */
	public int getSignatureLength() {
		return signatureLength;
	}
	
	/**
	 * Devuelve el conjunto de funciones de hash.
	 * Este conjunto no es modificable.
	 * @return conjunto de funciones de hash
	 */
	public Set<HashFunction> getHashFunctions() {
		return hashFunctions;
	}
	
	/**
	 * Crea una instancia de una clase hija de HashFunction.
	 * Para ello, invoca al constructor que recibe <code>int length</code>
	 * como unico parametro.
	 * @param name nombre completo de la clase
	 * @param length longitud de la firma para setear en la instancia
	 * @return una instancia nueva de una clase hija de HashFunction
	 * @throws SignatureConfigurationException si ocurre un error al instanciar
	 */
	private static HashFunction createInstance(String name, int length)
			throws SignatureConfigurationException {
		try {
			// Busco el constructor que recibe un int (length)
			// y con el creo mi instancia de HashFunction
			Constructor<?> c = Class.forName(name).getDeclaredConstructor(Integer.TYPE);
			return (HashFunction)c.newInstance(length);
		} catch (Exception e) {
			throw new SignatureConfigurationException("Error ald instanciar funcion de hash", e);
		}
	}
}
